/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.lucene.spatial.tier;

import org.apache.lucene.search.Filter;
import org.apache.lucene.spatial.geometry.LatLng;
import org.apache.lucene.spatial.geometry.LatLngRectangle;
import org.apache.lucene.spatial.tier.projection.CartesianTierPlotter;
import org.apache.lucene.spatial.tier.projection.Projector;
import org.apache.lucene.spatial.tier.projection.SinusoidalProjector;

/**
 * Builds instances of
 * {@link org.apache.lucene.spatial.tier.CartesianShapeFilter} for a given
 * latitude, longitude and distance
 */
public class CartesianPolyFilterBuilder {

	// Finer granularity than 1 mile isn't accurate with standard java math.
	public static final double MILES_FLOOR = 1.0;
	private static Projector PROJECTOR = new SinusoidalProjector();

	private final String tierPrefix;

	/**
	 * Creates a new CartesianPolyFilterBuilder that will construct instances of
	 * CartesianShapeFilter using the given prefix of the cartesian tier fields
	 * 
	 * @param tierPrefix
	 *            Prefix of the cartesian tier fields in documents. This will be
	 *            suffixed by the cartesian tier level id to determine which
	 *            fields the filter should access in documents
	 */
	public CartesianPolyFilterBuilder(String tierPrefix) {
		this.tierPrefix = tierPrefix;
	}

	/**
	 * Creates a new CartesianShapeFilter that will filter out documents outside
	 * of the given miles of the point defined by the given latitude and
	 * longitude
	 * 
	 * @param latitude
	 *            Latitude of the point at the centre of the search area
	 * @param longitude
	 *            Longitude of the point at the centre of the search area
	 * @param miles
	 *            Radius in miles around the point that documents must be in, in
	 *            order not to be filtered out
	 * @return CartesianShapeFilter that will filter out documents of the radius
	 *         of the point
	 */
	public Filter buildFilter(double latitude, double longitude, double miles) {
		Shape cartesianShape = getBoxShape(latitude, longitude, miles);
		return new CartesianShapeFilter(cartesianShape, tierPrefix
				+ cartesianShape.getTierID());
	}

	// ================================================= Helper Methods
	// ================================================

	/**
	 * Creates the CartesianShape that covers a circle with the given latitude
	 * and longitude at its centre, and the given radius in miles
	 * 
	 * @param latitude
	 *            Latitude of the centre of the circle
	 * @param longitude
	 *            Longitude of the centre of the circle
	 * @param miles
	 *            Radius of the circle
	 * @return CartesianShape that covers the circle defined by the lat/long and
	 *         radius
	 */
	private Shape getBoxShape(double latitude, double longitude, double miles) {
		if (miles < MILES_FLOOR) {
			miles = MILES_FLOOR;
		}
		LatLngRectangle box1 = LatLngRectangle.createIncludingLLRect(
				new LatLng(latitude, longitude), miles); // FIX : if box cross
															// pole(s) bevahior
															// is not
		// defined
		LatLng lowerLeft = box1.getLowerLeft();
		LatLng upperRight = box1.getUpperRight();

		double latUpperRight = upperRight.getLat();
		double latLowerLeft = lowerLeft.getLat();
		double longUpperRight = upperRight.getLng();
		double longLowerLeft = lowerLeft.getLng();

		CartesianTierPlotter ctp = new CartesianTierPlotter(miles, PROJECTOR);
		Shape shape = new Shape(ctp.getTierLevelId());

		if (longUpperRight < longLowerLeft) { // Box cross the 180 meridian
			addBoxes(shape, ctp, latLowerLeft, longLowerLeft, latUpperRight,
					LatLng.LONGITUDE_DEGREE_RANGE / 2);
			addBoxes(shape, ctp, latLowerLeft,
					-LatLng.LONGITUDE_DEGREE_RANGE / 2, latUpperRight,
					longUpperRight);
		} else {
			addBoxes(shape, ctp, latLowerLeft, longLowerLeft, latUpperRight,
					longUpperRight);
		}

		return shape;
	}

	private void addBoxes(Shape shape, CartesianTierPlotter tierPlotter,
			double lat1, double long1, double lat2, double long2) {
		double boxId1 = tierPlotter.getTierBoxId(lat1, long1);
		double boxId2 = tierPlotter.getTierBoxId(lat2, long2);

		double tierVert = tierPlotter.getTierVerticalPosDivider();

		int LongIndex1 = (int) Math.round(boxId1);
		int LatIndex1 = (int) Math.round((boxId1 - LongIndex1) * tierVert);

		int LongIndex2 = (int) Math.round(boxId2);
		int LatIndex2 = (int) Math.round((boxId2 - LongIndex2) * tierVert);

		int startLong, endLong;
		int startLat, endLat;

		if (LongIndex1 > LongIndex2) {
			startLong = LongIndex2;
			endLong = LongIndex1;
		} else {
			startLong = LongIndex1;
			endLong = LongIndex2;
		}

		if (LatIndex1 > LatIndex2) {
			startLat = LatIndex2;
			endLat = LatIndex1;
		} else {
			startLat = LatIndex1;
			endLat = LatIndex2;
		}

		int LatIndex, LongIndex;
		for (LongIndex = startLong; LongIndex <= endLong; LongIndex++) {
			for (LatIndex = startLat; LatIndex <= endLat; LatIndex++) {
				// create a boxId
				double boxId = LongIndex + LatIndex / tierVert;
				shape.addBox(boxId);
			}
		}
	}
}
